/*
 * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.oracle.truffle.espresso.processor;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

import com.oracle.truffle.espresso.processor.builders.AnnotationBuilder;
import com.oracle.truffle.espresso.processor.builders.ClassBuilder;
import com.oracle.truffle.espresso.processor.builders.ClassFileBuilder;
import com.oracle.truffle.espresso.processor.builders.FieldBuilder;
import com.oracle.truffle.espresso.processor.builders.JavadocBuilder;
import com.oracle.truffle.espresso.processor.builders.MethodBuilder;
import com.oracle.truffle.espresso.processor.builders.ModifierBuilder;
import com.oracle.truffle.espresso.processor.builders.SignatureBuilder;

/**
 * Helper class for creating all kinds of Substitution processor in Espresso. A processor need only
 * implement its own process method, along with providing three strings:
 * <li>The import sequence for the class generated.
 * <li>The constructor code for the class generated.
 * <li>The invoke method code for the class generated.
 * <p>
 * <p>
 * All other aspects of code generation are provided by this class.
 */
public abstract class EspressoProcessor extends BaseProcessor {
    /*- An example of a generated class is:
    
    // package com.oracle.truffle.espresso.substitutions.standard;
    
    import com.oracle.truffle.espresso.substitutions.JavaSubstitution;
    import com.oracle.truffle.espresso.runtime.staticobject.StaticObject;
    import com.oracle.truffle.espresso.substitutions.standard.Target_java_lang_Object.Init;
    
    /**
     * Generated by: {@link com.oracle.truffle.espresso.substitutions.standard.Target_java_lang_Object.Init}
     * /
    @com.oracle.truffle.espresso.substitutions.Collect(getter = "getFactory", value = com.oracle.truffle.espresso.substitutions.Substitution.class)
    public final class Target_java_lang_Object_Init__L extends JavaSubstitution {
    
        private static final JavaSubstitution.Factory FACTORY = new JavaSubstitution.Factory(
            "<init>",
            "Ljava/lang/Object;",
            "V",
            new String[]{
                        "Ljava/lang/Object;",
                    },
            true,
            com.oracle.truffle.espresso.substitutions.LanguageFilter.AlwaysValid.INSTANCE,
            (byte) 3,
            null,
            JavaSubstitution.lookupConstructor(Target_java_lang_Object_Init__L.class)
        );
    
        public static final JavaSubstitution.Factory getFactory() {
            return FACTORY;
        }
    
        @Child private Init node;
    
        @SuppressWarnings(value = "unused") public Target_java_lang_Object_Init__L() {
            super(FACTORY);
            this.node = com.oracle.truffle.espresso.substitutions.standard.Target_java_lang_ObjectFactory.InitNodeGen.create();
        }
    
        @Override
        public final JavaSubstitution split() {
            return FACTORY.create();
        }
    
        @Override
        public final Object invoke(Object[] args) {
            StaticObject arg0 = (StaticObject) args[0];
            this.node.execute(arg0);
    
            return StaticObject.NULL;
        }
    
        @Override
        public final void invokeInlined(VirtualFrame frame, int top, InlinedFrameAccess frameAccess) {
            StaticObject arg0 = EspressoFrame.popObject(frame, top - 1);
            this.node.execute(arg0);
        }
    
    }
     */

    /**
     * Does the actual work of the processor. The pattern used in espresso is:
     * <li>Initialize the {@link TypeElement} of the annotations that will be used, along with their
     * {@link AnnotationValue}, as necessary.
     * <li>Iterate over all methods annotated with what was returned by
     * {@link Processor#getSupportedAnnotationTypes()}, and process them so that each one spawns a
     * class.
     *
     * @see EspressoProcessor#commitSubstitution(Element, String, String, String)
     */
    abstract void processImpl(RoundEnvironment roundEnvironment);

    /**
     * Returns a list of expected imports of the current substitutor.
     * <p>
     * Note that the required imports vary between classes, as some might not be used, triggering
     * style issues, which is why this is delegated.
     *
     * @see EspressoProcessor#IMPORT_INTEROP_LIBRARY
     * @see EspressoProcessor#IMPORT_STATIC_OBJECT
     * @see EspressoProcessor#IMPORT_TRUFFLE_OBJECT
     */
    abstract List<String> expectedImports(String className, String targetMethodName, List<String> parameterTypeName, SubstitutionHelper helper);

    /**
     * Generates the builder corresponding to the Constructor for the current substitutor. In
     * particular, it should call its super class substitutor's constructor.
     *
     * @see EspressoProcessor#substitutor
     */
    abstract FieldBuilder generateFactoryConstructor(FieldBuilder factoryBuilder, String substitutorName, String factoryType, String substitutorType, String targetMethodName,
                    List<String> parameterTypeName,
                    SubstitutionHelper helper);

    /**
     * Generates the builder that corresponds to the code of the invoke method for the current
     * substitutor. Care must be taken to correctly unwrap and cast the given arguments (given in an
     * Object[]) so that they correspond to the substituted method's signature. Furthermore, all
     * TruffleObject nulls must be replaced with Espresso nulls (Null check can be done through
     * truffle libraries).
     *
     * @see EspressoProcessor#castTo(String, String)
     * @see EspressoProcessor#IMPORT_INTEROP_LIBRARY
     * @see EspressoProcessor#STATIC_OBJECT_NULL
     */
    abstract ClassBuilder generateInvoke(ClassBuilder classBuilder, String className, String targetMethodName, List<String> parameterTypeName, SubstitutionHelper helper);

    EspressoProcessor(String substitutionPackage, String substitutor) {
        this.substitutorPackage = substitutionPackage;
        this.substitutor = substitutor;
    }

    // Instance specific constants
    protected final String substitutorPackage;
    private final String substitutor;

    // Processor local info
    protected boolean done = false;

    // Special annotations
    TypeElement inject;
    private static final String INJECT = "com.oracle.truffle.espresso.substitutions.Inject";

    TypeElement noSafepoint;
    private static final String NO_SAFEPOINT = "com.oracle.truffle.espresso.jni.NoSafepoint";

    TypeElement substitutionProfiler;
    private static final String SUBSTITUTION_PROFILER = "com.oracle.truffle.espresso.substitutions.SubstitutionProfiler";

    TypeElement staticObject;
    protected static final String STATIC_OBJECT = "com.oracle.truffle.espresso.runtime.staticobject.StaticObject";

    TypeElement javaType;
    private static final String JAVA_TYPE = "com.oracle.truffle.espresso.substitutions.JavaType";

    TypeElement espressoLanguage;
    private static final String ESPRESSO_LANGUAGE = "com.oracle.truffle.espresso.EspressoLanguage";

    TypeElement meta;
    private static final String META = "com.oracle.truffle.espresso.meta.Meta";

    TypeElement espressoContext;
    private static final String ESPRESSO_CONTEXT = "com.oracle.truffle.espresso.runtime.EspressoContext";

    TypeElement truffleNode;
    private static final String TRUFFLE_NODE = "com.oracle.truffle.api.nodes.Node";

    TypeElement truffleObject;
    private static final String TRUFFLE_OBJECT = "com.oracle.truffle.api.interop.TruffleObject";

    // Global constants
    protected static final String FACTORY = "Factory";
    protected static final String FACTORY_FIELD_NAME = "FACTORY";

    static final String SUPPRESS_WARNINGS = "SuppressWarnings";
    static final String UNUSED = "unused";

    static final String GUARD = "guard";

    static final String STATIC_OBJECT_NULL = "StaticObject.NULL";

    static final String IMPORT_INTEROP_LIBRARY = "com.oracle.truffle.api.interop.InteropLibrary";
    static final String IMPORT_STATIC_OBJECT = STATIC_OBJECT;
    static final String IMPORT_TRUFFLE_OBJECT = TRUFFLE_OBJECT;
    static final String IMPORT_ESPRESSO_LANGUAGE = ESPRESSO_LANGUAGE;
    static final String IMPORT_META = "com.oracle.truffle.espresso.meta.Meta";
    static final String IMPORT_ESPRESSO_CONTEXT = ESPRESSO_CONTEXT;
    static final String IMPORT_PROFILE = "com.oracle.truffle.espresso.substitutions.SubstitutionProfiler";
    static final String COLLECT = "com.oracle.truffle.espresso.substitutions.Collect";

    static final String ESPRESSO_LANGUAGE_SIMPLE_NAME = "EspressoLanguage";
    static final String ESPRESSO_CONTEXT_SIMPLE_NAME = "EspressoContext";
    static final String META_SIMPLE_NAME = "Meta";

    static final String ESPRESSO_CONTEXT_VAR = "context";
    static final String ESPRESSO_CONTEXT_SETTER = String.format("%s %s = getContext();", ESPRESSO_CONTEXT, ESPRESSO_CONTEXT_VAR);

    static final String GET_ESPRESSO_LANGUAGE = "getLanguage()";
    static final String META_FROM_ESPRESSO_CONTEXT = String.format("%s.getMeta()", ESPRESSO_CONTEXT_VAR);

    static final String PROFILE_CLASS = "SubstitutionProfiler";
    static final String PROFILE_ARG_CALL = "this";

    static final String CREATE = "create";

    static final String SPLIT = "split";
    static final String GET_FACTORY = "getFactory";

    static final String ARGS_NAME = "args";
    static final String ARG_NAME = "arg";

    static final String SAFEPOINT_POLL = "com.oracle.truffle.api.TruffleSafepoint.poll(this);";

    static class InjectableType {
        static final InjectableType LANGUAGE = new InjectableType(ESPRESSO_LANGUAGE, GET_ESPRESSO_LANGUAGE);
        static final InjectableType CONTEXT = new InjectableType(ESPRESSO_CONTEXT, ESPRESSO_CONTEXT_VAR);
        static final InjectableType PROFILE = new InjectableType(SUBSTITUTION_PROFILER, PROFILE_ARG_CALL);

        static final List<InjectableType> knownTypes = List.of(LANGUAGE, CONTEXT, PROFILE);

        private final String qualifiedType;
        private final String simpleType;
        private final String getter;

        InjectableType(String qualifiedType) {
            this(qualifiedType, ESPRESSO_CONTEXT_VAR + ".get" + extractSimpleType(qualifiedType) + "()");
        }

        InjectableType(String qualifiedType, String getter) {
            this.qualifiedType = qualifiedType;
            this.simpleType = extractSimpleType(qualifiedType);
            this.getter = getter;
        }
    }

    public static NativeType classToType(TypeKind typeKind) {
        // @formatter:off
        switch (typeKind) {
            case BOOLEAN : return NativeType.BOOLEAN;
            case BYTE    : return NativeType.BYTE;
            case SHORT   : return NativeType.SHORT;
            case CHAR    : return NativeType.CHAR;
            case INT     : return NativeType.INT;
            case FLOAT   : return NativeType.FLOAT;
            case LONG    : return NativeType.LONG;
            case DOUBLE  : return NativeType.DOUBLE;
            case VOID    : return NativeType.VOID;
            default:
                return NativeType.OBJECT;
        }
        // @formatter:on
    }

    /**
     * Returns the name of the substituted method.
     */
    protected String getSubstutitutedMethodName(Element targetElement) {
        return targetElement.getSimpleName().toString();
    }

    /**
     * Returns the target method to be called by a substitution.
     *
     * Returns the targetElement itself for method substitutions; or the execute* method of the
     * Truffle node, for node substitutions.
     */
    protected ExecutableElement getTargetMethod(Element targetElement) {
        if (targetElement.getKind() == ElementKind.CLASS) {
            return findNodeExecute((TypeElement) targetElement);
        }
        return (ExecutableElement) targetElement;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
    }

    @Override
    public boolean doProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (done) {
            return false;
        }
        inject = getTypeElement(INJECT);
        noSafepoint = getTypeElement(NO_SAFEPOINT);
        staticObject = getTypeElement(STATIC_OBJECT);
        javaType = getTypeElement(JAVA_TYPE);
        espressoLanguage = getTypeElement(ESPRESSO_LANGUAGE);
        meta = getTypeElement(META);
        espressoContext = getTypeElement(ESPRESSO_CONTEXT);
        substitutionProfiler = getTypeElement(SUBSTITUTION_PROFILER);
        truffleNode = getTypeElement(TRUFFLE_NODE);
        truffleObject = getTypeElement(TRUFFLE_OBJECT);

        processImpl(roundEnv);
        done = true;
        return false;
    }

    // Utility Methods

    static AnnotationMirror getAnnotation(TypeMirror e, TypeElement type) {
        for (AnnotationMirror annotationMirror : e.getAnnotationMirrors()) {
            if (annotationMirror.getAnnotationType().asElement().equals(type)) {
                return annotationMirror;
            }
        }
        return null;
    }

    static AnnotationMirror getAnnotation(Element e, TypeElement type) {
        for (AnnotationMirror annotationMirror : e.getAnnotationMirrors()) {
            if (annotationMirror.getAnnotationType().asElement().equals(type)) {
                return annotationMirror;
            }
        }
        return null;
    }

    /**
     * For substitutions that use a node, find the execute* method. Suitable methods must be
     * non-private and be called execute*.
     */
    ExecutableElement findNodeExecute(TypeElement node) {
        if (!env().getTypeUtils().isSubtype(node.asType(), truffleNode.asType())) {
            getMessager().printMessage(Diagnostic.Kind.ERROR, "(Node) Substitution must inherit from " + truffleNode.getQualifiedName(), node);
        }
        ExecutableElement executeMethod = null;
        TypeElement curElement = node;
        while (true) {
            for (Element method : curElement.getEnclosedElements()) {
                if (method.getKind() == ElementKind.METHOD) {
                    // Match abstract non-private execute* .
                    if (method.getSimpleName().toString().startsWith("execute") &&
                                    !method.getModifiers().contains(Modifier.PRIVATE) &&
                                    method.getModifiers().contains(Modifier.ABSTRACT)) {
                        if (executeMethod != null) {
                            getMessager().printMessage(Diagnostic.Kind.ERROR, "Ambiguous execute* methods found: a unique non-private abstract execute* method is required", node);
                        }
                        executeMethod = (ExecutableElement) method;
                    }
                }
            }
            TypeMirror superClass = curElement.getSuperclass();
            if (TypeKind.NONE.equals(superClass.getKind())) {
                break;
            }
            curElement = asTypeElement(superClass);
        }
        if (executeMethod == null) {
            getMessager().printMessage(Diagnostic.Kind.ERROR, "Node execute* method not found", node);
        }
        return executeMethod;
    }

    List<InjectableType> getInjectedTypes(ExecutableElement method) {
        List<InjectableType> injectedTypes = new ArrayList<>();
        List<? extends VariableElement> params = method.getParameters();
        Types typeUtils = env().getTypeUtils();
        for (VariableElement e : params) {
            TypeMirror eType = e.asType();
            if (getAnnotation(eType, inject) != null) {
                // Simple name check is the best we can do, as there is no
                // 'Element.getQualifiedName'.
                String simpleTypeName = typeUtils.asElement(eType).getSimpleName().toString();
                boolean special = false;
                for (InjectableType knownType : InjectableType.knownTypes) {
                    if (knownType.simpleType.equals(simpleTypeName)) {
                        injectedTypes.add(knownType);
                        special = true;
                    }
                }
                if (!special) {
                    injectedTypes.add(new InjectableType(eType.toString()));
                }
            }
        }
        return injectedTypes;
    }

    boolean skipsSafepoint(Element target) {
        return getAnnotation(target, noSafepoint) != null;
    }

    boolean isActualParameter(VariableElement param) {
        return getAnnotation(param.asType(), inject) == null;
    }

    static boolean checkFirst(StringBuilder str, boolean first) {
        if (!first) {
            str.append(", ");
        }
        return false;
    }

    private static StringBuilder signatureSuffixBuilder(List<String> parameterTypes) {
        StringBuilder sb = new StringBuilder();
        sb.append("__");
        for (String param : parameterTypes) {
            // @formatter:off
            switch (param) {
                case "boolean" : sb.append("Z"); break;
                case "byte"    : sb.append("B"); break;
                case "char"    : sb.append("C"); break;
                case "int"     : sb.append("I"); break;
                case "long"    : sb.append("J"); break;
                case "float"   : sb.append("F"); break;
                case "double"  : sb.append("D"); break;
                case "void"    : sb.append("V"); break;
                default:
                    assert (param.startsWith("[") || param.startsWith("L")) && param.endsWith(";");
                    sb.append("L"); break;
            }
            // @formatter:on
        }
        return sb;
    }

    static String getSubstitutorClassName(String className, String methodName, List<String> parameterTypes) {
        return String.format("%s_%s%s", className, methodName, signatureSuffixBuilder(parameterTypes));
    }

    static String castTo(String obj, String clazz) {
        if (clazz.equals("Object")) {
            return obj;
        }
        return "(" + clazz + ") " + obj;
    }

    static String extractSimpleType(String arg) {
        // The argument can be a fully qualified type e.g. java.lang.String, int, long...
        // Or an annotated type e.g. "(@com.example.Annotation :: long)",
        // "(@com.example.Annotation :: java.lang.String)".
        // javac always includes annotations, ecj does not.

        // Purge enclosing parentheses.
        String result = arg;
        if (result.startsWith("(")) {
            result = result.substring(1, result.length() - 1);
        }

        // Purge leading annotations.
        String[] parts = result.split("::");
        result = parts[parts.length - 1].trim();
        // Prune additional spaces produced by javac 11.
        parts = result.split(" ");
        result = parts[parts.length - 1].trim();

        // Get unqualified name.
        int beginIndex = result.lastIndexOf('.');
        if (beginIndex >= 0) {
            result = result.substring(beginIndex + 1);
        }
        return result;
    }

    static void setEspressoContextVar(MethodBuilder methodBuilder, SubstitutionHelper helper) {
        if (helper.needsContextInjection()) {
            methodBuilder.addBodyLine(EspressoProcessor.ESPRESSO_CONTEXT_SETTER);
        }
    }

    /**
     * Injects the meta information in the substitution call.
     */
    static boolean appendInvocationMetaInformation(StringBuilder str, boolean first, SubstitutionHelper helper) {
        boolean f = first;
        for (InjectableType injectedType : helper.injectedTypes) {
            f = injectType(str, injectedType, f);
        }
        return f;
    }

    // Commits a single substitution.
    void commitSubstitution(Element method, String targetPackage, String substitutorName, String classFile) {
        try {
            // Create the file
            JavaFileObject file = processingEnv.getFiler().createSourceFile(targetPackage + "." + substitutorName, method);
            Writer wr = file.openWriter();
            wr.write(classFile);
            wr.close();
        } catch (IOException ex) {
            /* nop */
        }
    }

    private static JavadocBuilder generateGeneratedBy(List<String> parameterTypes, SubstitutionHelper helper) {
        JavadocBuilder javadocBuilder = new JavadocBuilder();

        if (helper.isNodeTarget()) {
            javadocBuilder.addGeneratedByLine(helper.getNodeTarget().getQualifiedName());
            return javadocBuilder;
        }

        SignatureBuilder linkSignature = new SignatureBuilder().withName(helper.getEnclosingClass().getQualifiedName().toString() + "#" + helper.getMethodTarget().getSimpleName());

        for (String param : parameterTypes) {
            linkSignature.addParam(param);
        }
        for (InjectableType injectedType : helper.injectedTypes) {
            linkSignature.addParam(injectedType.qualifiedType);
        }

        javadocBuilder.addGeneratedByLine(linkSignature);
        return javadocBuilder;
    }

    static SignatureBuilder generateNativeSignature(NativeType[] signature) {
        SignatureBuilder sb = new SignatureBuilder().withName("NativeSignature.create");
        for (NativeType t : signature) {
            sb.addParam("NativeType." + t);
        }
        return sb;
    }

    // @formatter:off
    /**
     * Generates the following.
     * 
     * @Collect(ImplAnnotation.class)
     * public static final class Factory extends SUBSTITUTOR.Factory {
     *     private Factory() {
     *         super(
     *             "SUBSTITUTED_METHOD",
     *             "SUBSTITUTION_CLASS",
     *             "RETURN_TYPE",
     *             new String[]{
     *                 SIGNATURE
     *             },
     *             HAS_RECEIVER
     *         );
     *     }
     *     @Override
     *     public final SUBSTITUTOR create() {
     *         return new substitutorName();
     *     }
     * }
     */
    // @formatter:on
    private FieldBuilder generateFactory(String substitutorName, String targetMethodName, List<String> parameterTypeName, SubstitutionHelper helper) {
        String factoryType = substitutor + "." + FACTORY;
        FieldBuilder factory = new FieldBuilder(factoryType, FACTORY_FIELD_NAME) //
                        .withQualifiers(new ModifierBuilder().asPrivate().asFinal().asStatic()); //
        generateFactoryConstructor(factory, substitutorName, factoryType, substitutor, targetMethodName, parameterTypeName, helper);
        return factory;
    }

    private static void generateChildInstanceField(ClassBuilder cb, SubstitutionHelper helper) {
        if (helper.isNodeTarget()) {
            FieldBuilder field = new FieldBuilder(helper.getNodeTarget().getSimpleName(), "node") //
                            .withAnnotation(new AnnotationBuilder("Child")) //
                            .withQualifiers(new ModifierBuilder().asPrivate());
            cb.withField(field);
        }
    }

    /**
     * Generates the constructor for the substitutor.
     */
    private static MethodBuilder generateConstructor(String substitutorName, SubstitutionHelper helper) {
        MethodBuilder constructor = new MethodBuilder(substitutorName) //
                        .asConstructor() //
                        .withModifiers(new ModifierBuilder().asPublic());
        constructor.addBodyLine("super(", FACTORY_FIELD_NAME, ");");
        if (helper.isNodeTarget()) {
            TypeElement enclosing = (TypeElement) helper.getNodeTarget().getEnclosingElement();
            constructor.addBodyLine("this.node = ", enclosing.getQualifiedName(), "Factory.", helper.getNodeTarget().getSimpleName(), "NodeGen", ".create();");
        }
        return constructor;
    }

    static String generateLookupConstructor(String className, String substitutorType) {
        return substitutorType + ".lookupConstructor(" + className + ".class)";
    }

    static boolean injectType(StringBuilder str, InjectableType injectableType, boolean first) {
        checkFirst(str, first);
        str.append(injectableType.getter);
        return false;
    }

    /**
     * Creates the substitutor.
     *
     * @param className The name of the host class where the substituted method is found.
     * @param targetMethodName The name of the substituted method.
     * @param parameterTypeName The list of *Host* parameter types of the substituted method.
     * @param helper A helper structure.
     * @return The string forming the substitutor.
     */
    String spawnSubstitutor(String substitutorName, String targetPackage, String className, String targetMethodName, List<String> parameterTypeName, SubstitutionHelper helper) {
        ClassFileBuilder substitutorFile = new ClassFileBuilder() //
                        .withCopyright() //
                        .inPackage(targetPackage);

        // Prepare imports
        List<String> expectedImports = expectedImports(substitutorName, targetMethodName, parameterTypeName, helper);

        // Add imports (filter useless import)
        for (String toImport : expectedImports) {
            String maybePackage = toImport.substring(0, toImport.lastIndexOf('.'));
            if (!targetPackage.equals(maybePackage)) {
                substitutorFile.withImport(toImport);
            }
        }

        ClassBuilder substitutorClass = new ClassBuilder(substitutorName) //
                        .withSuperClass(substitutor) //
                        .withJavaDoc(generateGeneratedBy(parameterTypeName, helper)) //
                        .withQualifiers(new ModifierBuilder().asPublic().asFinal()) //
                        .withAnnotation(new AnnotationBuilder(COLLECT).withLineBreak() //
                                        .withValue("value", helper.getCollectTarget().toString() + ".class", false) //
                                        .withValue("getter", GET_FACTORY)) //
                        .withField(generateFactory(substitutorName, targetMethodName, parameterTypeName, helper)) //
                        .withMethod(new MethodBuilder(GET_FACTORY).withModifiers(new ModifierBuilder().asPublic().asStatic().asFinal()) //
                                        .withReturnType(substitutor + "." + FACTORY) //
                                        .addBodyLine("return ", FACTORY_FIELD_NAME, ";"));

        if (helper.isNodeTarget()) {
            generateChildInstanceField(substitutorClass, helper);
        }

        MethodBuilder constructor = generateConstructor(substitutorName, helper) //
                        .withAnnotation(new AnnotationBuilder(SUPPRESS_WARNINGS).withValue("value", UNUSED));
        substitutorClass.withMethod(constructor);

        substitutorClass.withMethod(generateSplit());

        generateInvoke(substitutorClass, className, targetMethodName, parameterTypeName, helper);

        substitutorFile.withClass(substitutorClass);
        return substitutorFile.build();
    }

    /**
     * Injects override of 'split()' methods.
     */
    private MethodBuilder generateSplit() {
        MethodBuilder method = new MethodBuilder(SPLIT) //
                        .withOverrideAnnotation() //
                        .withModifiers(new ModifierBuilder().asPublic().asFinal()) //
                        .withReturnType(substitutor) //
                        .addBodyLine("return ", FACTORY_FIELD_NAME, ".", CREATE, "();");
        return method;
    }

    public Messager getMessager() {
        return processingEnv.getMessager();
    }
}
